package abel.wlmr.client;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import abel.util.HexString;
import abel.util.StdBcdNumber;
import abel.wlmr.config.MeterStation;
import abel.wlmr.config.MsaConfigLoader;
import abel.wlmr.config.PacketSender;
import abel.wlmr.config.PacketSender.PacketTimes;
import abel.wlmr.model.WlmrBasePart;
import abel.wlmr.model.WlmrBaseUnit;
import abel.wlmr.model.WlmrPacket;
import abel.wlmr.model.type.AfnType;
import abel.wlmr.model.type.C1DateTimeType;
import abel.wlmr.model.type.DataIdBody;
import abel.wlmr.model.type.Erc12Body;
import abel.wlmr.model.type.Erc21Body;
import abel.wlmr.model.type.Erc60Body;
import abel.wlmr.model.type.FnActiveType;
import abel.wlmr.model.type.FnProactiveType;
import abel.wlmr.model.type.MeterConfigBody;
import abel.wlmr.model.type.TermErrorType;
import abel.wlmr.model.type.WlmrControlField.CtrlAcd;
import abel.wlmr.model.type.WlmrControlField.CtrlDir;
import abel.wlmr.model.type.WlmrControlField.CtrlPrm;
import abel.wlmr.model.type.WlmrErrorType;
import abel.wlmr.model.unit.Afn00F3Unit;
import abel.wlmr.model.unit.Afn00Part;
import abel.wlmr.model.unit.Afn02F3Unit;
import abel.wlmr.model.unit.Afn02Part;
import abel.wlmr.model.unit.Afn04DownPart;
import abel.wlmr.model.unit.Afn04F401Unit;
import abel.wlmr.model.unit.Afn04Part;
import abel.wlmr.model.unit.Afn05F31Unit;
import abel.wlmr.model.unit.Afn05UpPart;
import abel.wlmr.model.unit.Afn09F1UpUnit;
import abel.wlmr.model.unit.Afn09UpPart;
import abel.wlmr.model.unit.AfnFirstF402Unit;
import abel.wlmr.model.unit.AfnFirstF403Unit;
import abel.wlmr.model.unit.AfnFirstF404Unit;
import abel.wlmr.model.unit.AfnFirstPart;
import abel.wlmr.model.unit.AfnQueryPart;
import abel.wlmr.model.unit.AfnSecondF409Unit;
import abel.wlmr.model.unit.AfnSecondF410Unit;
import abel.wlmr.model.unit.AfnSecondPart;
import abel.wlmr.model.unit.AfnThirdF1_2DownUnit;
import abel.wlmr.model.unit.AfnThirdF1_2Unit;
import abel.wlmr.model.unit.AfnThirdPart;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;

public class WlmrClientHandler extends ChannelInboundHandlerAdapter {
    private static final Logger log = LoggerFactory.getLogger(WlmrClientHandler.class);

    private static final AttributeKey<Byte> seqKey = AttributeKey.valueOf("seq");

    private static final AttributeKey<PacketSender> senderKey = AttributeKey.valueOf("sender");

    private static final String STATION_JSON = "config/hat.json";

    public WlmrClientHandler() {
    }

    /* (non-Javadoc)
     * @see io.netty.channel.ChannelInboundHandlerAdapter#channelActive(io.netty.channel.ChannelHandlerContext)
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("Active {}", ctx.name());

        Attribute<Byte> seq = ctx.channel().attr(seqKey);
        seq.setIfAbsent((byte) 0);

        // sender
        PacketSender sender = new PacketSender();
        ctx.channel().attr(senderKey).setIfAbsent(sender);

        MsaConfigLoader.checkStationFile(STATION_JSON);
        MeterStation station = MsaConfigLoader.getStationMap().values().iterator().next();

        // login
        {
            WlmrPacket r = new WlmrPacket(log);
            r.getAddr().setA1(station.getAddr().getA1());
            r.getAddr().setA2(station.getAddr().getA2());

            r.getControl().setDir(CtrlDir.Up);
            r.getControl().setPrm(CtrlPrm.Active);
            r.getControl().setFat(FnActiveType.Heart);

            r.setAfn(AfnType.Heart);

            r.getFseq().setCon(true);
            r.getFseq().setSeq(nextSequence(ctx));
            r.getFseq().setTpv(true);

            r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));

            // part
            Afn02Part part1 = new Afn02Part();
            part1.getId().getDt().setTypeList(Afn02Part.F_LOGIN);

            r.setPart1(part1);

            // send out
            sender.queuePacket(r);
        }

        // sync
        {
            WlmrPacket r = new WlmrPacket(log);
            r.getAddr().setA1(station.getAddr().getA1());
            r.getAddr().setA2(station.getAddr().getA2());

            r.getControl().setDir(CtrlDir.Up);
            r.getControl().setPrm(CtrlPrm.Active);
            r.getControl().setFat(FnActiveType.UserData);

            r.setAfn(AfnType.Ctrl);

            r.getFseq().setCon(true);
            r.getFseq().setTpv(true);

            // part
            Afn05UpPart part1 = new Afn05UpPart();
            part1.getId().getDt().setTypeList(Afn05UpPart.F_SYNC);

            r.setPart1(part1);

            // u31
            Afn05F31Unit u31 = new Afn05F31Unit();
            u31.setStdTime(new C1DateTimeType(DateTime.now(), true));

            part1.firstUnit(u31);

            // send out
            sender.queuePacket(r);
        }
    }

    /* (non-Javadoc)
     * @see io.netty.channel.ChannelInboundHandlerAdapter#channelRead(io.netty.channel.ChannelHandlerContext, java.lang.Object)
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof ByteBuf) {
            ByteBuf buf = (ByteBuf) msg;
            log.info("Down: {}", buf.readCharSequence(buf.readableBytes(), Charset.defaultCharset()));
        } else if (msg instanceof WlmrPacket) {
            // ...
            WlmrPacket p = (WlmrPacket) msg;
            log.info("Down {}: {}", p.getAfn(), p);

            // if response (may be Confirm / First / Second / ...)
            if (p.getControl().getPrm() == CtrlPrm.Proactive) {
                PacketTimes req = ctx.channel().attr(senderKey).get().gotReply(p.getFseq().getSeq());

                if (req != null) {
                    log.info("SEQ {}: Up {} & Down {}.",
                        p.getFseq().getSeq(), p.getAfn(), req.getPacket().getAfn());
                }
            }

            switch (p.getAfn()) {
            case Term:
                replyTerm(ctx, p);
                break;

            case Set:
                replyConfigMeter(ctx, p);
                break;

            case Query:
                replyQuery(ctx, p);
                break;

            case Third:
                replyThird(ctx, p);
                break;

            default:
                log.info("No need to reply {}: F{}...",
                    p.getAfn(), p.getPart1().getId().getDt().firstType());
                break;
            }
        } else {
            log.info("Down {}: {}.", ctx.name(), msg);
        }
    }

    private void replyThird(ChannelHandlerContext ctx, WlmrPacket p) {
        MeterStation station = MsaConfigLoader.getStationMap().values().iterator().next();

        {
            WlmrBasePart part1 = p.getPart1();
            AfnThirdF1_2DownUnit u1 = (AfnThirdF1_2DownUnit) part1.firstUnit();

            log.info("Event type: {}, Pm: {}, Pn:{}", part1.getId().getDt().firstType(), u1.getPm(), u1.getPn());
        }

        {
            WlmrPacket r = new WlmrPacket(log);
            r.setAddr(station.getAddr());

            r.setAfn(AfnType.Third);

            r.getControl().setDir(CtrlDir.Up);
            r.getControl().setPrm(CtrlPrm.Proactive);
            r.getControl().setPat(FnProactiveType.UserData);

            r.getFseq().setTpv(true);

            // time
            r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));

            // part1
            AfnThirdPart part12 = new AfnThirdPart();
            part12.getId().getDt().setTypeList(AfnThirdPart.F_IMPORTANT_EVENT, AfnThirdPart.F_NORMAL_EVENT);

            r.setPart1(part12);

            // unit 1
            AfnThirdF1_2Unit u1 = new AfnThirdF1_2Unit();
            part12.addUnitGroup((short) 0, u1);

            u1.setEc1((short) 1);
            u1.setEc2((short) 2);
            u1.setPm((short) 0);
            u1.setPn((short) 2);

            // event
            Erc12Body erc12 = new Erc12Body();
            erc12.setOccurTime(StdBcdNumber.fromMeterC15Joda(new DateTime(2017, 2, 14, 5, 20)));
            erc12.setStartStop((byte) 0);
            erc12.setPn((short) 1);

            u1.getErcList().add(erc12);

            // next event
            Erc60Body erc60 = new Erc60Body();
            erc60.setPn((short) 2);
            erc60.getWarnThreshold().flowPoint(20000.0);
            erc60.setOccurTime(StdBcdNumber.fromMeterC15Joda(DateTime.now()));

            u1.getErcList().add(erc60);

            // unit 2
            AfnThirdF1_2Unit u2 = new AfnThirdF1_2Unit();
            part12.addUnitGroup((short) 0, u2);
            u2.setEc1((short) 2);
            u2.setEc2((short) 1);
            u2.setPm((short) 2);
            u2.setPn((short) 3);

            // event
            Erc21Body erc21 = new Erc21Body();
            erc21.setFailureNo(TermErrorType.Carrier);
            erc21.setOpenTime(StdBcdNumber.fromMeterC15Joda(new DateTime(2017, 2, 18, 3, 3)));

            u2.getErcList().add(erc21);

            // send
            ctx.writeAndFlush(r);
        }
    }

    private void replyQuery(ChannelHandlerContext ctx, WlmrPacket p) {
        MeterStation station = MsaConfigLoader.getStationMap().values().iterator().next();
        
        if (new Random().nextBoolean()) {
            // just Deny
            WlmrPacket r = new WlmrPacket(log);
            r.setAfn(AfnType.Verify);
            r.getAddr().setA1(station.getAddr().getA1());
            r.getAddr().setA2(station.getAddr().getA2());

            r.getControl().setDir(CtrlDir.Up);
            r.getControl().setPrm(CtrlPrm.Proactive);
            r.getControl().setPat(FnProactiveType.Deny);

            r.getFseq().setSeq(p.getFseq().getSeq());
            r.getFseq().setTpv(true);
            r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));

            // part1
            Afn00Part part1 = new Afn00Part();
            part1.getId().getDt().setTypeList(Afn00Part.F_DENY);

            r.setPart1(part1);

            // send out
            ctx.writeAndFlush(r);

            return;
        }

        List<DataIdBody> list = new ArrayList<>();

        // query
        short daNo = -1;
        for (WlmrBasePart part1 : p.getPartList()) {
            for (short dt : part1.getId().getDt().typeList()) {
                if (dt == AfnQueryPart.F_METER_CONFIG) {
                    daNo = part1.getId().getDa().firstAddr();
                    if (daNo > 0) {
                        break;
                    }
                } else {
                    log.warn("un-processed {}-F{}", p.getAfn(), dt);
                }
            }
        }

        if (daNo < 0) {
            log.warn("invalid daNo for {}", p.getAfn());
            return;
        }

        // find meter.
        MeterConfigBody meter = null;
        for (MeterConfigBody m1 : station.getMeterList()) {
            if (daNo == m1.getDaNo()) {
                meter = m1;
                break;
            }
        }

        // compose meter
        WlmrPacket r = new WlmrPacket(log);
        r.getAddr().setA1(station.getAddr().getA1());
        r.getAddr().setA2(station.getAddr().getA2());

        r.getControl().setDir(CtrlDir.Up);
        r.getControl().setPrm(CtrlPrm.Proactive);

        if (meter == null) {
            r.getControl().setPat(FnProactiveType.Deny);

            r.setAfn(AfnType.Verify);

            r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));
            r.getFseq().setTpv(true);

            // 00-F3
            Afn00Part part1 = new Afn00Part();
            part1.getId().getDt().setTypeList(Afn00Part.F_SOME);

            r.setPart1(part1);

            // unit
            Afn00F3Unit unit1 = new Afn00F3Unit();
            unit1.setConfirmingAfn(AfnType.Query);
            unit1.getErrList().add(WlmrErrorType.Error);
            DataIdBody da = new DataIdBody();
            da.getDa().setAddrList(daNo);
            unit1.getIdList().add(da);

            part1.firstUnit(unit1);

            // seq.
            r.getFseq().setSeq(p.getFseq().getSeq());

            // send out
            ByteBuf buf = ctx.alloc().buffer();
            r.appendBuf(buf);
            // log
            log.info("Up {}: {}\nHex: {}",
                r.getAfn(), r, HexString.bytesToHexStringWithBlank(buf.duplicate()));
            ctx.writeAndFlush(buf);
        } else {
            r.getControl().setPat(FnProactiveType.UserData);

            r.setAfn(AfnType.Query);

            r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));
            r.getFseq().setTpv(true);

            // 0A-F401
            AfnQueryPart part1 = new AfnQueryPart();
            part1.getId().getDt().setTypeList(AfnQueryPart.F_METER_CONFIG);
            r.setPart1(part1);

            // F401
            Afn04F401Unit unit1 = new Afn04F401Unit();
            unit1.setMeterCnt(1);
            unit1.getMeterList().add(meter);

            part1.firstUnit(unit1);

            // seq
            r.getFseq().setSeq(p.getFseq().getSeq());

            // send out
            ByteBuf buf = ctx.alloc().buffer();
            r.appendBuf(buf);
            // log
            log.info("Up {}: {}\nHex: {}",
                r.getAfn(), r, HexString.bytesToHexStringWithBlank(buf.duplicate()));
            ctx.writeAndFlush(buf);
        }
    }

    private void replyConfigMeter(ChannelHandlerContext ctx, WlmrPacket p) {
        MeterStation station = MsaConfigLoader.getStationMap().values().iterator().next();

        // compose confirm
        WlmrPacket r = new WlmrPacket(log);
        r.getAddr().setA1(station.getAddr().getA1());
        r.getAddr().setA2(station.getAddr().getA2());

        r.getControl().setDir(CtrlDir.Up);
        r.getControl().setPrm(CtrlPrm.Proactive);
        r.getControl().setPat(FnProactiveType.Confirm);

        r.setAfn(AfnType.Verify);
        r.getFseq().setSeq(p.getFseq().getSeq());
        r.getFseq().setTpv(true);

        r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));

        // part1
        Afn00Part rp1 = new Afn00Part();
        rp1.getId().getDt().setTypeList(Afn00Part.F_SOME);

        r.setPart1(rp1);

        // unit1
        Afn00F3Unit ru3 = new Afn00F3Unit();
        rp1.firstUnit(ru3);
        ru3.setConfirmingAfn(p.getAfn());

        // confirm one by one
        int cnt = 0;
        for (WlmrBasePart part1 : p.getPartList()) {
            if (part1 instanceof Afn04DownPart) {
                Afn04DownPart p1 = (Afn04DownPart) part1;

                for (List<WlmrBaseUnit> us1 : p1.getUnitList()) {
                    for (WlmrBaseUnit u1 : us1) {
                        if (u1 instanceof Afn04F401Unit) {
                            Afn04F401Unit du4 = (Afn04F401Unit) u1;

                            for (int i = 0; i < du4.getMeterCnt(); i++) {
                                MeterConfigBody meter = du4.getMeterList().get(i);

                                DataIdBody mid = new DataIdBody();
                                mid.getDa().setAddrList(meter.getMeterNo());

                                // delete if Da-No = 0.
                                if (0 == meter.getDaNo()) {
                                    deleteMeter(ctx, meter);
                                }

                                mid.getDt().setTypeList(Afn04Part.F_METER_CONFIG);

                                ru3.getIdList().add(mid);
                                ru3.getErrList().add(cnt % 2 == 0 ? WlmrErrorType.Correct : WlmrErrorType.Error);

                                ++cnt;
                            }
                        } else {
                            log.warn("un-processed {}: {}", u1.getClass(), u1);
                        }
                    }
                }
            } else {
                log.error("un-processed part: {}", part1);
            }
        }

        // about to send
        ByteBuf buf = ctx.alloc().buffer();
        r.appendBuf(buf);
        log.info("Up {}: {}\nHex: {}",
            r.getAfn(), r, HexString.bytesToHexStringWithBlank(buf.duplicate()));
        ctx.writeAndFlush(buf);
    }

    /**
     * Delete meter: just set its Da-No to 0.
     * 
     * @param ctx
     * @param meter
     */
    private void deleteMeter(ChannelHandlerContext ctx, MeterConfigBody meter) {
        MeterStation station = MsaConfigLoader.getStationMap().values().iterator().next();

        int idx = -1;
        for (int i = 0; i < station.getMeterList().size(); i++) {
            MeterConfigBody m1 = station.getMeterList().get(i);
            if (m1.getMeterNo() == meter.getMeterNo()) {
                m1.setDaNo((short) 0);

                idx = i;
                break;
            }
        }

        if (idx < 0) {
            log.warn("Meter not found to delete: {}", meter);
        } else {
            log.warn("Meter is deleted: {}", meter);
        }
    }

    private void replyTerm(ChannelHandlerContext ctx, WlmrPacket p) {
        MeterStation station = MsaConfigLoader.getStationMap().values().iterator().next();

        // send term info
        WlmrPacket r = new WlmrPacket(log);
        r.getAddr().setA1(station.getAddr().getA1());
        r.getAddr().setA2(station.getAddr().getA2());

        r.getControl().setDir(CtrlDir.Up);
        r.getControl().setPrm(CtrlPrm.Proactive);
        r.getControl().setPat(FnProactiveType.UserData);

        r.setAfn(AfnType.Term);

        r.getFseq().setTpv(true);
        r.getFseq().setSeq(p.getFseq().getSeq());

        r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));

        // part
        Afn09UpPart part1 = new Afn09UpPart();
        part1.getId().getDt().setTypeList(Afn09UpPart.F_TERM_INFO);

        Afn09F1UpUnit unit1 = new Afn09F1UpUnit();
        unit1.setHardReleaseDate(StdBcdNumber.fromMeterC20Joda(new DateTime(2015, 1, 1, 0, 0)));
        unit1.setHardVersion("0603");
        unit1.setMachineNo("12345678");
        unit1.setManufacturer("2345");
        unit1.setProtocolVersion("2016");
        unit1.setSoftReleaseDate(StdBcdNumber.fromMeterC20Joda(new DateTime(2016, 12, 1, 0, 0)));
        unit1.setSoftVersion("0100");
        unit1.setTermCapacity("ABCDEFGHIJK");

        part1.firstUnit(unit1);

        // set to packet.
        r.setPart1(part1);

        // try to send
        ByteBuf buf = ctx.alloc().buffer();
        r.appendBuf(buf);
        log.info("Up {}: {}\nHex: {}",
            r.getAfn(), r, HexString.bytesToHexStringWithBlank(buf.duplicate()));
        ctx.writeAndFlush(buf);
    }

    /* (non-Javadoc)
     * @see io.netty.channel.ChannelInboundHandlerAdapter#userEventTriggered(io.netty.channel.ChannelHandlerContext, java.lang.Object)
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            // idle
            MsaConfigLoader.checkStationFile(STATION_JSON);

            Attribute<PacketSender> sender = ctx.channel().attr(senderKey);

            // check if first
            PacketTimes pt = sender.get().nextPacket();

            // Term / Query / First / Second
            if (pt != null) {
                pt.getPacket().getFseq().setSeq(nextSequence(ctx));
                pt.getPacket().getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));

                // send out
                ctx.writeAndFlush(pt.getPacket());
                sender.get().sendOut(pt);
            } else {
                // build demo data
                switch (new Random().nextInt(10)) {
                case 1:
                    queueFirst(ctx);
                    break;

                case 2:
                    queueSecondFdd(ctx);
                    break;

                case 3:
                    queueSecondFdm(ctx);
                    break;

                case 4:
                    queueThird(ctx);
                    break;

                case 5:
                    queueHeart(ctx);
                    break;

                case 6:
                    invokeCtrlSync(ctx);
                    break;

                default:
                    break;

                }
            }
        } else {
            super.userEventTriggered(ctx, evt);
            return;
        }
    }

    /**
     * Invoke every 200 times.
     * 
     * @param ctx
     */
    private void invokeCtrlSync(ChannelHandlerContext ctx) {
        if (new Random().nextInt(200) != 1) {
            return;
        }

        MeterStation station = MsaConfigLoader.getStationMap().values().iterator().next();

        WlmrPacket p = new WlmrPacket(log);
        p.setAddr(station.getAddr());

        p.getControl().setDir(CtrlDir.Up);
        p.getControl().setPrm(CtrlPrm.Active);
        p.getControl().setFat(FnActiveType.Heart);

        p.setAfn(AfnType.Ctrl);

        p.getFseq().setCon(true);
        p.getFseq().setTpv(true);

        // part1
        Afn02Part part1 = new Afn02Part();
        part1.getId().getDt().setTypeList(Afn02Part.F_HEART);

        p.setPart1(part1);

        // unit1
        Afn02F3Unit unit1 = new Afn02F3Unit();

        part1.firstUnit(unit1);

        p.getFseq().setSeq(nextSequence(ctx));
        p.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now().minusMinutes(125)));

        // should invoke ctrl-sync.
        ctx.writeAndFlush(p);
    }

    private void queueHeart(ChannelHandlerContext ctx) {
        MeterStation station = MsaConfigLoader.getStationMap().values().iterator().next();

        WlmrPacket p = new WlmrPacket(log);
        p.setAddr(station.getAddr());

        p.getControl().setDir(CtrlDir.Up);
        p.getControl().setPrm(CtrlPrm.Active);
        p.getControl().setFat(FnActiveType.Heart);

        p.setAfn(AfnType.Heart);

        p.getFseq().setTpv(true);

        // part1
        Afn02Part part1 = new Afn02Part();
        part1.getId().getDt().setTypeList(Afn02Part.F_HEART);

        p.setPart1(part1);

        // unit1
        Afn02F3Unit unit1 = new Afn02F3Unit();

        part1.firstUnit(unit1);

        // event.
        p.getControl().setAcd(CtrlAcd.HasEvents);

        p.getEventCount().setEc1((short) 1);
        p.getEventCount().setEc2((short) 2);

        // queue
        ctx.channel().attr(senderKey).get().queuePacket(p);
    }

    private void queueFirst(ChannelHandlerContext ctx) {
        MeterStation station = MsaConfigLoader.getStationMap().values().iterator().next();
        Random random = new Random();

        int size = station.getMeterList().size();
        int no = new Random().nextInt(size);
        MeterConfigBody meter1 = station.getMeterList().get(no);
        if (meter1.getDaNo() < 1) {
            return;
        }

        if (random.nextBoolean()) {
            queueFirstVelocity(ctx, station, meter1);
        } else {
            queueFirstWhole(ctx, station, meter1);
        }
    }

    private void queueThird(ChannelHandlerContext ctx) {
        // TODO Auto-generated method stub

    }

    private void queueSecondFdm(ChannelHandlerContext ctx) {
        MeterStation station = MsaConfigLoader.getStationMap().values().iterator().next();

        int size = station.getMeterList().size();
        int no = new Random().nextInt(size);
        MeterConfigBody meter1 = station.getMeterList().get(no);
        if (meter1.getDaNo() < 1) {
            return;
        }

        WlmrPacket r = new WlmrPacket(log);
        r.getAddr().setA1(station.getAddr().getA1());
        r.getAddr().setA2(station.getAddr().getA2());

        r.getControl().setDir(CtrlDir.Up);
        r.getControl().setPrm(CtrlPrm.Active);
        r.getControl().setFat(FnActiveType.Second);

        r.getFseq().setCon(true);
        r.getFseq().setTpv(true);

        r.setAfn(AfnType.Second);

        // part
        AfnSecondPart part1 = new AfnSecondPart();
        r.setPart1(part1);

        part1.getId().getDa().setAddrList(meter1.getDaNo());
        part1.getId().getDt().setTypeList(AfnSecondPart.F_FROZEN_MONTH);

        // unit
        AfnSecondF410Unit unit = new AfnSecondF410Unit();
        unit.setFrozeTdm(StdBcdNumber.fromMeterC21Joda(DateTime.now().minusMonths(1)));
        unit.getForwardFLow().flowPoint(1599.60);
        unit.setReadingTime(StdBcdNumber.fromMeterC15Joda(DateTime.now().minusDays(12)));

        part1.firstUnit(unit);

        // finally set time & seq.
        r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));
        r.getFseq().setSeq(nextSequence(ctx));

        //try to send.
        ctx.channel().attr(senderKey).get().queuePacket(r);
    }

    private void queueSecondFdd(ChannelHandlerContext ctx) {
        MeterStation station = MsaConfigLoader.getStationMap().values().iterator().next();

        int size = station.getMeterList().size();
        int no = new Random().nextInt(size);
        MeterConfigBody meter1 = station.getMeterList().get(no);
        if (meter1.getDaNo() < 1) {
            return;
        }

        WlmrPacket r = new WlmrPacket(log);
        r.getAddr().setA1(station.getAddr().getA1());
        r.getAddr().setA2(station.getAddr().getA2());

        r.getControl().setDir(CtrlDir.Up);
        r.getControl().setPrm(CtrlPrm.Active);
        r.getControl().setFat(FnActiveType.Second);

        r.getFseq().setCon(true);
        r.getFseq().setTpv(true);

        r.setAfn(AfnType.Second);

        // part
        AfnSecondPart part1 = new AfnSecondPart();
        r.setPart1(part1);

        part1.getId().getDa().setAddrList(meter1.getDaNo());
        part1.getId().getDt().setTypeList(AfnSecondPart.F_FROZEN_DAY);

        // unit
        AfnSecondF409Unit unit = new AfnSecondF409Unit();
        unit.setFrozeTdd(StdBcdNumber.fromMeterC20Joda(DateTime.now().minusDays(1)));
        unit.getForwardFLow().flowPoint(1605.35);
        unit.setReadingTime(StdBcdNumber.fromMeterC15Joda(DateTime.now().minusHours(12)));

        part1.firstUnit(unit);

        // finally set time & seq.
        r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));
        r.getFseq().setSeq(nextSequence(ctx));

        //try to send.
        ctx.channel().attr(senderKey).get().queuePacket(r);
    }

    private void queueFirstVelocity(ChannelHandlerContext ctx, MeterStation station, MeterConfigBody meter1) {
        WlmrPacket r = new WlmrPacket(log);
        r.getAddr().setA1(station.getAddr().getA1());
        r.getAddr().setA2(station.getAddr().getA2());

        r.getControl().setDir(CtrlDir.Up);
        r.getControl().setPrm(CtrlPrm.Active);
        r.getControl().setFat(FnActiveType.First);

        r.getFseq().setCon(true);
        r.getFseq().setTpv(true);

        r.setAfn(AfnType.First);
        r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));

        // part1
        AfnFirstPart part1 = new AfnFirstPart();
        part1.getId().getDa().setAddrList(meter1.getDaNo());
        part1.getId().getDt().setTypeList(AfnFirstPart.F_VOLUME);

        r.setPart1(part1);

        // F404
        AfnFirstF404Unit unit = new AfnFirstF404Unit();
        unit.setReadingTime(StdBcdNumber.fromMeterC15Joda(DateTime.now()));
        unit.getVelocity().flowPoint(1817.56);

        part1.firstUnit(unit);

        // send
        ctx.channel().attr(senderKey).get().queuePacket(r);
    }

    private void queueFirstWhole(ChannelHandlerContext ctx, MeterStation station, MeterConfigBody meter1) {
        WlmrPacket r = new WlmrPacket(log);
        r.getAddr().setA1(station.getAddr().getA1());
        r.getAddr().setA2(station.getAddr().getA2());

        r.getControl().setDir(CtrlDir.Up);
        r.getControl().setPrm(CtrlPrm.Active);
        r.getControl().setFat(FnActiveType.First);

        r.getFseq().setCon(true);
        r.getFseq().setTpv(true);

        r.setAfn(AfnType.First);
        r.getTpv().setSendTime(StdBcdNumber.fromMeterC16Joda(DateTime.now()));

        // part1
        AfnFirstPart part1 = new AfnFirstPart();
        part1.getId().getDa().setAddrList(meter1.getDaNo());
        part1.getId().getDt().setTypeList(AfnFirstPart.F_METER_STATUS, AfnFirstPart.F_VELOCITY, AfnFirstPart.F_VOLUME);

        r.setPart1(part1);

        // F402
        AfnFirstF402Unit u402 = new AfnFirstF402Unit();
        u402.setReadingTime(StdBcdNumber.fromMeterC15Joda(DateTime.now()));

        part1.addUnitGroup((short) 0, u402);

        // F403
        AfnFirstF403Unit u403 = new AfnFirstF403Unit();
        u403.setReadingTime(StdBcdNumber.fromMeterC15Joda(DateTime.now()));

        u403.getVelocity().flowPoint(3.01);
        u403.getPressure().flowPoint(37.81);

        part1.addUnitGroup((short) 0, u403);

        // F404
        AfnFirstF404Unit unit = new AfnFirstF404Unit();
        unit.setReadingTime(StdBcdNumber.fromMeterC15Joda(DateTime.now()));
        unit.getVelocity().flowPoint(1817.56);

        part1.addUnitGroup((short) 0, unit);

        // send
        ctx.channel().attr(senderKey).get().queuePacket(r);
    }

    /* (non-Javadoc)
     * @see io.netty.channel.ChannelInboundHandlerAdapter#exceptionCaught(io.netty.channel.ChannelHandlerContext, java.lang.Throwable)
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("Error for {}: {}", ctx.name(), cause.getMessage());

        log.info("Error for " + ctx.name(), cause);

        ctx.close();
    }

    /**
     * return next seq - often old + 1. But may be unpredictable.
     * 
     * @param ctx
     * @return
     */
    private byte nextSequence(ChannelHandlerContext ctx) {
        Attribute<Byte> seq = ctx.channel().attr(seqKey);
        byte next = seq.get();
        for (int i = 0; i < 8; i++) {
            if (seq.compareAndSet(next, (byte) (next + 1))) {
                return (byte) ((next + 1) & 0x0F);
            }

            next = (byte) ((1 + next) & 0x0F);
        }

        return next;
    }
}
